Autor: Michael Volk
Alle beschriebenen Schritte werden in dem Jupyter-Notebook Kfz-Haftpflichtversicherungsschaeden-Modellierung.ipynb auf Basis von Python 3.9 durchgeführt. Das Notebook kann auch als reine HTML-Version mit oder ohne Python-Code-Darstellung aufgerufen werden:
Ziel ist es Modelle bzgl. Schadenwahrscheinlichkeit und Schadenaufwand auf Basis eines öffentlich verfügbaren, französischen Kfz-Haftpflichtversicherungs-Datensatzes abzuleiten und diese dann zu trainieren und zu validieren.
Zunächst wird mit der Exploration der Daten mittels univariater und multivariater Analysen begonnen. Im Laufe der Datenexploration und Datenanalyse werden bereits Datenaufbereitungen vorgenommen, die für die Visualisierung und spätere Modellierung als Grundlage dienen. Schließlich erfolgt die Modellierung um Schadenwahrscheinlichkeit und Schadenaufwand für einen Vertrag mit gegebenen Risikomerkmalen prognostizieren zu können. Sowohl für die Modellierung der Schadenwahrscheinlichkeit als auch für den Schadenaufwand werden jeweils folgende 3 Modelle in unterschiedlichen Konfigurationen verwendet:
Dazu werden die Daten in Trainings- und Testdaten aufgespalten und weiter verarbeitet in eine für das jeweilige Modell passende Form. Die Modelle werden dann jeweils auf Basis der Trainingsdaten trainiert. Das jeweilige trainierte Modell wird dann auf die Testdaten angewendet und geprüft anhand verschiedener Kritierien im Vergleich zu den anderen Modellen:
Zudem erfolgt eine Analyse der wichtigsten Merkmale bzgl. dem XGBoost-Modell und bzgl. eines einfachen Entscheidungsbaums.
Der französische Kfz-Haftpflichtversicherungs-Datensatz besteht aus den beiden Dateien freMTPL2freq.csv und freMTPL2sev.csv, welche jeweils eine Tabelle beinhalten, die über die Vertragsnummer miteinander verbunden werden können. Die Daten wurden von Karan Sarpal über Kaagle zur Verfügung gestellt und von dort herunter geladen (s. [1]). Sie wurden aber wohl ursprünglich von Christophe Dutang und Arthur Charpentier veröffentlicht auf Basis der Daten eines unbekannten privaten Versicherers (s. [7], Abschnitt freMTPL). Die folgende Beschreibung der Daten wurde weitestgehend aus [7] übernommen.
Datei freMTPL2freq.csv enthält die Vertragsdaten und Schadenanzahl bzgl. dem jeweiligem Versicherungsvertrag. Datei freMTPL2sev.csv enthält den Schadenaufwand bzgl. dem jeweiligem Versicherungsvertrag.
Beschreibung der Spalten in Datei freMTPL2freq.csv:
IDpol: Vertragsnummer der Versicherung, zwecks Verknüpfung mit Vertragsnummer in freMTPL2sev.csvClaimNb: Anzahl an Schäden innerhalb des Versicherungszeitraums (=Spalte Exposure)Exposure: Zeitraum in Jahreseinheiten in welcher der Vertrag versichert warArea: Der Dichtewert der Stadtgemeinde, in der der Autofahrer lebt: von "A" für ländliche Gegend bis "F" für Stadtzentrum.VehPower: Motorleistung (geordnete Werte)VehAge: Alter des FahrzeugesDrivAge: Alter des FahrersBonusMalus: Bonus/Malus-Score, zwischen 50 und 350: <100 bedeutet Bonus, >100 bedeutet Malus in Frankreich.VehBrand: Automarke (unbekannte Kategorien)VehGas: Kraftstoff des Fahrzeugs (Diesel oder regulär)Density: Die Einwohnerdichte (Anzahl der Einwohner pro Quadratkilometer) der Stadt, in der der Autofahrer wohntRegion: Die Verwaltungsbezirke in Frankreich (basierend auf der Klassifikation 1970-2015).Beschreibung der Spalten in freMTPL2sev.csv:
IDpol: Vertragsnummer der Versicherung, zwecks Verknüpfung mit Vertragsnummer in freMTPL2freq.csvClaimAmount: Schadenaufwand in €import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from pandas_profiling import ProfileReport
import seaborn as sns
import math
from copy import deepcopy
from sklearn.pipeline import make_pipeline
from sklearn import preprocessing
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, OrdinalEncoder
from sklearn.preprocessing import StandardScaler, KBinsDiscretizer
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyRegressor
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_poisson_deviance
from sklearn.metrics import mean_gamma_deviance
from sklearn.linear_model import PoissonRegressor
from sklearn.linear_model import GammaRegressor
from sklearn.tree import DecisionTreeRegressor, plot_tree
import xgboost
from xgboost import XGBRegressor
from xgboost import plot_importance
from sklearn.utils import gen_even_slices
C:\Users\Hauptkonto\anaconda3\envs\testpy39\lib\site-packages\xgboost\compat.py:36: FutureWarning: pandas.Int64Index is deprecated and will be removed from pandas in a future version. Use pandas.Index with the appropriate dtype instead. from pandas import MultiIndex, Int64Index
Laden des Datensatzes freMTPL2freq.csv in ein pandas-Dataframe (Tabelle):
filename = r'freMTPL2freq.csv'
df_freq = pd.read_csv(filename)
Laden des Datensatzes freMTPL2sev.csv in ein pandas-Dataframe (Tabelle):
filename = r'freMTPL2sev.csv'
df_sev = pd.read_csv(filename)
df_freq
| IDpol | ClaimNb | Exposure | VehPower | VehAge | DrivAge | BonusMalus | VehBrand | VehGas | Area | Density | Region | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1 | 0.10000 | 5 | 0 | 55 | 50 | B12 | Regular | D | 1217 | Rhone-Alpes |
| 1 | 3.0 | 1 | 0.77000 | 5 | 0 | 55 | 50 | B12 | Regular | D | 1217 | Rhone-Alpes |
| 2 | 5.0 | 1 | 0.75000 | 6 | 2 | 52 | 50 | B12 | Diesel | B | 54 | Picardie |
| 3 | 10.0 | 1 | 0.09000 | 7 | 0 | 46 | 50 | B12 | Diesel | B | 76 | Aquitaine |
| 4 | 11.0 | 1 | 0.84000 | 7 | 0 | 46 | 50 | B12 | Diesel | B | 76 | Aquitaine |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 678008 | 6114326.0 | 0 | 0.00274 | 4 | 0 | 54 | 50 | B12 | Regular | E | 3317 | Provence-Alpes-Cotes-D'Azur |
| 678009 | 6114327.0 | 0 | 0.00274 | 4 | 0 | 41 | 95 | B12 | Regular | E | 9850 | Ile-de-France |
| 678010 | 6114328.0 | 0 | 0.00274 | 6 | 2 | 45 | 50 | B12 | Diesel | D | 1323 | Rhone-Alpes |
| 678011 | 6114329.0 | 0 | 0.00274 | 4 | 0 | 60 | 50 | B12 | Regular | B | 95 | Bourgogne |
| 678012 | 6114330.0 | 0 | 0.00274 | 7 | 6 | 29 | 54 | B12 | Diesel | B | 65 | Aquitaine |
678013 rows × 12 columns
df_sev
| IDpol | ClaimAmount | |
|---|---|---|
| 0 | 1552 | 995.20 |
| 1 | 1010996 | 1128.12 |
| 2 | 4024277 | 1851.11 |
| 3 | 4007252 | 1204.00 |
| 4 | 4046424 | 1204.00 |
| ... | ... | ... |
| 26634 | 3254353 | 1200.00 |
| 26635 | 3254353 | 1800.00 |
| 26636 | 3254353 | 1000.00 |
| 26637 | 2222064 | 767.55 |
| 26638 | 2254065 | 1500.00 |
26639 rows × 2 columns
df_freq: Insgesamt 678013 Zeilen und 12 Spaltendf_sev: Insgesamt 26639 Zeilen und 2 SpaltenSomit treten Schäden im Verhältnis zur Gesamtzahl der Verträge selten auf
df_freq.dtypes
IDpol float64 ClaimNb int64 Exposure float64 VehPower int64 VehAge int64 DrivAge int64 BonusMalus int64 VehBrand object VehGas object Area object Density int64 Region object dtype: object
df_sev.dtypes
IDpol int64 ClaimAmount float64 dtype: object
Somit sind insgesamt in den beiden Tabellen Spalten mit numerischen Werten (float und integer) sowie kategorialen Werten enthalten.
df_freq.isnull().sum()
IDpol 0 ClaimNb 0 Exposure 0 VehPower 0 VehAge 0 DrivAge 0 BonusMalus 0 VehBrand 0 VehGas 0 Area 0 Density 0 Region 0 dtype: int64
df_sev.isnull().sum()
IDpol 0 ClaimAmount 0 dtype: int64
Somit fehlen in beiden Tabellen für sich betrachtet keine Werte.
df_freq.duplicated(subset=['IDpol']).any()
False
df_sev.duplicated(subset=['IDpol']).any()
True
Somit Duplikate bzgl. IDpol in df_sev vorhanden. Zeige deshalb Duplikate bzgl. Spalte IDpol an:
df_sev_IDpolDupl = df_sev['IDpol'].value_counts()
df_sev_IDpolDupl
2262511 66
2282134 36
2227533 25
2220367 24
2277846 23
..
1170241 1
1091072 1
1049695 1
1121141 1
2254065 1
Name: IDpol, Length: 24950, dtype: int64
Anzahl Werte von IDpol bei denen es Duplikate gibt:
df_sev_IDpolDupl[df_sev_IDpolDupl > 1].count()
1379
Damit ergibt sich die Frage, ob pro Vertrag und Schadenfall ein Eintrag in Tabelle df_sev steht.
Wenn dem so ist, müsste ClaimNb in df_freq gleich der Anzahl an Zeilen von Tabelle df_sev sein.
Dies wird somit geprüft:
df_freq.ClaimNb.sum()
36102
df_sev.IDpol.count()
26639
Da die Schadensanzahl in df_freq größer ist als die Anzahl der Zeilen in df_sev, existiert nicht generell pro Vertrag und Schadenfall ein Eintrag in Tabelle df_sev. Dafür könnte es folgende Gründe geben:
df_sev fehlenZwecks näherer Untersuchung wird deshalb die Verteilung der Anzahl an gleichen Vertragsnummern in df_sev mit der Verteilung der Schadensanzahl ClaimNb in df_freq verglichen.
Verteilung der Anzahl an gleichen Vertragsnummern in df_sev:
df_sev_IDpolDupl.value_counts()
1 23571 2 1298 3 62 4 5 11 2 5 2 66 1 36 1 25 1 24 1 23 1 21 1 16 1 9 1 8 1 6 1 Name: IDpol, dtype: int64
Verteilung der Schadensanzahl ClaimNb in df_freq:
df_freq.loc[df_freq['ClaimNb'] > 0, 'ClaimNb'].value_counts()
1 32178 2 1784 3 82 4 7 11 3 5 2 6 1 8 1 16 1 9 1 Name: ClaimNb, dtype: int64
df_freq.loc[df_freq['ClaimNb'] > 0, 'ClaimNb'].count()
34060
32178 unterschiedliche Verträge in df_freq weisen genau einen Schaden auf, aber nur 23571 Verträge in df_sev weisen genau einen Schadenbetrag auf. Dies deutet darauf hin, dass Verträge in df_sev fehlen für die es einen Schaden gab gemäß df_freq. Deshalb wird im Folgenden untersucht wieviele Verträge aus df_sev in df_freq fehlen und umgekehrt.
Auf Grund der bisherigen Erkenntnisse wird aber zunächst df_sev zu einer bzgl. der Vertragsnummer nicht redundanten Tabelle df_sev_nr zusammengefasst. Dabei werden alle Schadenbeträge ClaimAmount eines Vertrages summiert.
df_sev_nr¶df_sev_nr = df_sev.groupby(by='IDpol').agg({'ClaimAmount':sum}).reset_index()
# Teste ob Summe über ClaimAmount gleich ist zwischen df_sev_nr und df_sev:
assert df_sev.ClaimAmount.sum() == df_sev_nr.ClaimAmount.sum(), 'Ungleiche Summe in ClaimAmount!'
df_sev_nr
| IDpol | ClaimAmount | |
|---|---|---|
| 0 | 139 | 303.00 |
| 1 | 190 | 1981.84 |
| 2 | 414 | 1456.55 |
| 3 | 424 | 10834.00 |
| 4 | 463 | 3986.67 |
| ... | ... | ... |
| 24945 | 6113521 | 1324.40 |
| 24946 | 6113793 | 1769.88 |
| 24947 | 6113817 | 1288.28 |
| 24948 | 6113834 | 12230.40 |
| 24949 | 6113971 | 1452.00 |
24950 rows × 2 columns
df_freq und df_sev_nr anhand der Vertragsnummer¶Verbinden der beiden Tabellen df_freq und df_sev_nr anhand der Vertragsnummer mittels Outer-Join, so dass keine Verträge verloren gehen:
df = pd.merge(df_freq, df_sev_nr, on='IDpol', how='outer')
df
| IDpol | ClaimNb | Exposure | VehPower | VehAge | DrivAge | BonusMalus | VehBrand | VehGas | Area | Density | Region | ClaimAmount | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 0.10 | 5.0 | 0.0 | 55.0 | 50.0 | B12 | Regular | D | 1217.0 | Rhone-Alpes | NaN |
| 1 | 3.0 | 1.0 | 0.77 | 5.0 | 0.0 | 55.0 | 50.0 | B12 | Regular | D | 1217.0 | Rhone-Alpes | NaN |
| 2 | 5.0 | 1.0 | 0.75 | 6.0 | 2.0 | 52.0 | 50.0 | B12 | Diesel | B | 54.0 | Picardie | NaN |
| 3 | 10.0 | 1.0 | 0.09 | 7.0 | 0.0 | 46.0 | 50.0 | B12 | Diesel | B | 76.0 | Aquitaine | NaN |
| 4 | 11.0 | 1.0 | 0.84 | 7.0 | 0.0 | 46.0 | 50.0 | B12 | Diesel | B | 76.0 | Aquitaine | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 678014 | 2227533.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 64093.76 |
| 678015 | 2262511.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 151800.59 |
| 678016 | 2277846.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 73440.72 |
| 678017 | 2282134.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 330920.05 |
| 678018 | 2286775.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 84017.43 |
678019 rows × 13 columns
Anzahl Verträge für die Schadenbetrag vorliegt aber keine sonstigen Merkmale:
df.IDpol[df['ClaimAmount'].notnull() & df['ClaimNb'].isnull()].count()
6
Anzahl Verträge für die Schadenbetrag vorliegt aber Schadenanzahl = 0:
df.IDpol[df['ClaimAmount'].notnull() & (df['ClaimNb'] == 0)].count()
0
Anzahl Verträge für die kein Schadenbetrag vorliegt aber Schadenanzahl > 0:
df.IDpol[df['ClaimAmount'].isnull() & (df['ClaimNb'] > 0)].count()
9116
Anzahl Verträge für die Schadenbetrag vorliegt und Schadenanzahl > 0:
df.IDpol[df['ClaimAmount'].notnull() & (df['ClaimNb'] > 0)].count()
24944
round(24944/678019, 3)
0.037
round(34060/678019, 3)
0.05
34060-24944
9116
Zusammenfassung:
Sehr geringe Anzahl von 6 Verträgen, für die ein Schadenbetrag vorliegt aber keine sonstigen Merkmale. Im Verhältnis dazu große Anzahl von 24944 Verträgen, für die Schadenbetrag vorliegt und Schadenanzahl > 0 (~3,7% aller Verträge). Es wird angenommen, dass die 6 Verträge hinsichtlich der Ausprägung der (nicht vorhandenen) Risikomerkmale ähnlich sind bzgl. der Verträge mit vorhandenen Risikomerkmalen und vorhandenem Schadenbetrag. Deshalb können die 6 Verträge ohne nennenswerte Auswirkungen aus der Tabelle gelöscht werden - wird im nächsten Schritt gemacht.
Relativ große Anzahl von 9116 Verträgen für die kein Schadenbetrag vorliegt aber Schadenanzahl > 0. Das ist problematisch in der Größenordnung. Ursachen könnten sein:
Da keine zusätzlichen Informationen vorliegen muss für die Modellierung von einer der beiden Ursachen ausgegangen werden. Deshalb wird für die Modellierung von der Ursache ausgegangen, dass der Schadenbetrag fälschlicherweise fehlt. Man könnte zwar auch 2 Modellierungsvorgänge unter Annahme jeweils einer der beiden erstgenannten Ursachen durchführen, darauf wird hier aber verzichtet.
df¶Löschen der Zeilen aus Tabelle df für die Schadenbetrag vorliegt aber keine sonstigen Merkmale (6 Zeilen, s. vorheriger Abschnitt):
df.drop(df[df['ClaimAmount'].notnull() & df['ClaimNb'].isnull()].index, inplace=True)
# Test ob Zeilenanzahl von df gleich der von df_freq
assert df.shape[0] == df_freq.shape[0], 'Zeilenanzahl ungleich zu df_freq!'
Zielgröße ClaimAmountperClaimNb (Schadenbetrag pro Schadensfall) hinzufügen:
df["ClaimAmountperClaimNb"] = df["ClaimAmount"] / df["ClaimNb"]
Zielgröße Frequency (Schadenhäufigkeit pro Versicherungsjahr) hinzufügen:
df["Frequency"] = df["ClaimNb"] / df["Exposure"]
df
| IDpol | ClaimNb | Exposure | VehPower | VehAge | DrivAge | BonusMalus | VehBrand | VehGas | Area | Density | Region | ClaimAmount | ClaimAmountperClaimNb | Frequency | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 0.10000 | 5.0 | 0.0 | 55.0 | 50.0 | B12 | Regular | D | 1217.0 | Rhone-Alpes | NaN | NaN | 10.000000 |
| 1 | 3.0 | 1.0 | 0.77000 | 5.0 | 0.0 | 55.0 | 50.0 | B12 | Regular | D | 1217.0 | Rhone-Alpes | NaN | NaN | 1.298701 |
| 2 | 5.0 | 1.0 | 0.75000 | 6.0 | 2.0 | 52.0 | 50.0 | B12 | Diesel | B | 54.0 | Picardie | NaN | NaN | 1.333333 |
| 3 | 10.0 | 1.0 | 0.09000 | 7.0 | 0.0 | 46.0 | 50.0 | B12 | Diesel | B | 76.0 | Aquitaine | NaN | NaN | 11.111111 |
| 4 | 11.0 | 1.0 | 0.84000 | 7.0 | 0.0 | 46.0 | 50.0 | B12 | Diesel | B | 76.0 | Aquitaine | NaN | NaN | 1.190476 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 678008 | 6114326.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 54.0 | 50.0 | B12 | Regular | E | 3317.0 | Provence-Alpes-Cotes-D'Azur | NaN | NaN | 0.000000 |
| 678009 | 6114327.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 41.0 | 95.0 | B12 | Regular | E | 9850.0 | Ile-de-France | NaN | NaN | 0.000000 |
| 678010 | 6114328.0 | 0.0 | 0.00274 | 6.0 | 2.0 | 45.0 | 50.0 | B12 | Diesel | D | 1323.0 | Rhone-Alpes | NaN | NaN | 0.000000 |
| 678011 | 6114329.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 60.0 | 50.0 | B12 | Regular | B | 95.0 | Bourgogne | NaN | NaN | 0.000000 |
| 678012 | 6114330.0 | 0.0 | 0.00274 | 7.0 | 6.0 | 29.0 | 54.0 | B12 | Diesel | B | 65.0 | Aquitaine | NaN | NaN | 0.000000 |
678013 rows × 15 columns
In diesem Abschnitt werden die Merkmale und Zielgrößen univariat analysiert bzgl. Lagemaße und Histogramme
Lagemaße bzgl. der numerischen Merkmale:
df.describe()
| IDpol | ClaimNb | Exposure | VehPower | VehAge | DrivAge | BonusMalus | Density | ClaimAmount | ClaimAmountperClaimNb | Frequency | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 6.780130e+05 | 678013.000000 | 678013.000000 | 678013.000000 | 678013.000000 | 678013.000000 | 678013.000000 | 678013.000000 | 2.494400e+04 | 2.494400e+04 | 678013.000000 |
| mean | 2.621857e+06 | 0.053247 | 0.528750 | 6.454631 | 7.044265 | 45.499122 | 59.761502 | 1792.422405 | 2.401749e+03 | 2.221368e+03 | 0.263964 |
| std | 1.641783e+06 | 0.240117 | 0.364442 | 2.050906 | 5.666232 | 14.137444 | 15.636658 | 3958.646564 | 3.026475e+04 | 2.899257e+04 | 4.593916 |
| min | 1.000000e+00 | 0.000000 | 0.002732 | 4.000000 | 0.000000 | 18.000000 | 50.000000 | 1.000000 | 1.000000e+00 | 1.000000e+00 | 0.000000 |
| 25% | 1.157951e+06 | 0.000000 | 0.180000 | 5.000000 | 2.000000 | 34.000000 | 50.000000 | 92.000000 | 7.497875e+02 | 7.105550e+02 | 0.000000 |
| 50% | 2.272152e+06 | 0.000000 | 0.490000 | 6.000000 | 6.000000 | 44.000000 | 50.000000 | 393.000000 | 1.172000e+03 | 1.172000e+03 | 0.000000 |
| 75% | 4.046274e+06 | 0.000000 | 0.990000 | 7.000000 | 11.000000 | 55.000000 | 64.000000 | 1658.000000 | 1.346400e+03 | 1.228080e+03 | 0.000000 |
| max | 6.114330e+06 | 16.000000 | 2.010000 | 15.000000 | 100.000000 | 100.000000 | 230.000000 | 27000.000000 | 4.075401e+06 | 4.075401e+06 | 732.000117 |
Häufigkeitsverteilungen bzgl. der numerischen Merkmale anhand von Histogrammen (normal und kumulativ) um Verteilung der numerischen Merkmale besser einschätzen zu können. Bzgl. manchen Merkmalen erfolgt zusätzlich detailliertere Darstellung bzgl. eines eingeschränkten Wertebereichs und/oder eine Log-Skalierung
fig, ax = plt.subplots(16, 2, figsize=(10,50))
#Normale Histogramme
sns.histplot(df['ClaimNb'], ax=ax[0][0]).set_title('ClaimNb')
sns.histplot(df['ClaimNb'][df['ClaimNb']>0], ax=ax[1][0]).set_title('ClaimNb > 0')
sns.histplot(df['Frequency'], ax=ax[2][0]).set_title('Frequency')
sns.histplot(df['Frequency'][(df['Frequency']>0) & (df['Frequency']<=10)], ax=ax[3][0]).set_title('0 < Frequency <= 10')
sns.histplot(df['Exposure'], ax=ax[4][0]).set_title('Exposure')
sns.histplot(df['VehPower'], ax=ax[5][0]).set_title('VehPower')
sns.histplot(df['VehAge'], ax=ax[6][0]).set_title('VehAge')
sns.histplot(df['DrivAge'], ax=ax[7][0]).set_title('DrivAge')
sns.histplot(df['BonusMalus'], ax=ax[8][0]).set_title('BonusMalus')
sns.histplot(df['BonusMalus'][df['BonusMalus']>50], ax=ax[9][0]).set_title('BonusMalus > 50')
sns.histplot(df['BonusMalus'][df['BonusMalus']>100], ax=ax[10][0]).set_title('BonusMalus > 100')
sns.histplot(df['Density'], log_scale=True, ax=ax[11][0]).set_title('Density')
sns.histplot(df['ClaimAmount'], log_scale=True, ax=ax[12][0]).set_title('ClaimAmount')
sns.histplot(df['ClaimAmount'][df['ClaimAmount']<=10000], bins=20, ax=ax[13][0]).set_title('ClaimAmount <= 10000')
sns.histplot(df['ClaimAmountperClaimNb'], log_scale=True, ax=ax[14][0]).set_title('ClaimAmountperClaimNb')
sns.histplot(df['ClaimAmountperClaimNb'][df['ClaimAmountperClaimNb']<=10000], bins=20, ax=ax[15][0]).set_title('ClaimAmountperClaimNb <= 10000')
#Kumulative Histogramme
sns.histplot(df['ClaimNb'], stat='percent', cumulative='True', ax=ax[0][1]).set_title('ClaimNb')
sns.histplot(df['ClaimNb'][df['ClaimNb']>0], stat='percent', cumulative='True', ax=ax[1][1]).set_title('ClaimNb > 0')
sns.histplot(df['Frequency'], stat='percent', cumulative='True', ax=ax[2][1]).set_title('Frequency')
sns.histplot(df['Frequency'][(df['Frequency']>0) & (df['Frequency']<=10)], stat='percent', cumulative='True', ax=ax[3][1]).set_title('0 < Frequency <= 10')
sns.histplot(df['Exposure'], stat='percent', cumulative='True', ax=ax[4][1]).set_title('Exposure')
sns.histplot(df['VehPower'], stat='percent', cumulative='True', ax=ax[5][1]).set_title('VehPower')
sns.histplot(df['VehAge'], stat='percent', cumulative='True', ax=ax[6][1]).set_title('VehAge')
sns.histplot(df['DrivAge'], stat='percent', cumulative='True', ax=ax[7][1]).set_title('DrivAge')
sns.histplot(df['BonusMalus'], stat='percent', cumulative='True', ax=ax[8][1]).set_title('BonusMalus')
sns.histplot(df['BonusMalus'][df['BonusMalus']>50], stat='percent', cumulative='True', ax=ax[9][1]).set_title('BonusMalus > 50')
sns.histplot(df['BonusMalus'][df['BonusMalus']>100], stat='percent', cumulative='True', ax=ax[10][1]).set_title('BonusMalus > 100')
sns.histplot(df['Density'], log_scale=True, stat='percent', cumulative='True', ax=ax[11][1]).set_title('Density')
sns.histplot(df['ClaimAmount'], log_scale=True, stat='percent', cumulative='True', ax=ax[12][1]).set_title('ClaimAmount')
sns.histplot(df['ClaimAmount'][df['ClaimAmount']<=10000], bins=20, stat='percent', cumulative='True', ax=ax[13][1]).set_title('ClaimAmount <= 10000')
sns.histplot(df['ClaimAmountperClaimNb'], log_scale=True, stat='percent', cumulative='True', ax=ax[14][1]).set_title('ClaimAmountperClaimNb')
sns.histplot(df['ClaimAmountperClaimNb'][df['ClaimAmountperClaimNb']<=10000], bins=20, stat='percent', cumulative='True', ax=ax[15][1]).set_title('ClaimAmountperClaimNb <= 10000')
fig.tight_layout()
Auffälligkeiten bzgl. der Lagemaße und Verteilung der numerischen Merkmale:
ClaimNb: für ~95% der Verträge ist Wert = 0. Das ist nicht überraschend: Es ist zu erwarten, dass Großteil der Verträge in der relativ kurzen betrachteten Versicherungszeitraumsspanne von <= 1 Jahr keine Schadensansprüche aufweist. Werte > 3 treten so selten auf (Anzahl < 10), dass sie wohl als Ausreißer interpretierbar sind => für die Modellierung werden Werte > 3 auf 3 gesetzt werden.Frequency: Der Mittelwert von ~26% sagt nichts über die Schadenhäufigkeit des Portfolios aus, da dafür der gewichtete Mittelwert gebildet werden muss mit dem Merkmal Exposure als Gewicht - diese Berechnung wird noch erfolgen. Auf Grund der häufig sehr kurzen Versicherungszeiträume einzelner Verträge, weisen diese im Falle eines Schadensereignisses eine als hoch berechnete Schadenhäufigkeit aus - das erklärt zum einen die hohen Werte. Zum anderen führen auch die die Ausreißer bzgl. ClaimNb zu hohen Werten => für die Modellierung wird Frequency auf Basis der dann in den Wertebereich eingeschränkten ClaimNb berechnet werden.Exposure: für ~75% der Verträge ist Wert < 1 Jahr, kleinster sogar bei nur 1 Tag. Ist es üblich in Frankreich, dass viele Kfz-Haftplichtversicherungsverträge für Zeitraum < 1 Jahr abgeschlossen werden? Zudem ist Anteil der Werte > 1 Jahr vernachlässigbar klein. Die Vermutung ist, dass es sich um Datenfehler handelt und der eigentliche Beobachtungszeitraum bzgl. der Versicherungsverträge nur 1 Jahr beträgt => für die Modellierung werden Werte > 1 auf 1 gesetzt werden.VehAge: Anteil der Autos, die älter als 20 Jahre sind, ist vernachlässigbar klein => für die Modellierung werden Werte > 20 auf 20 gesetzt werden.DriveAge: Anteil der Fahrer, die älter als 90 Jahre sind, ist vernachlässigbar klein => für die Modellierung werden Werte > 90 auf 90 gesetzt werden.BonusMalus: ~55% der Verträge weisen den niedrigsten Wert von 50 auf. Nur noch sehr wenig Verträge ab Wert > 110. Es fällt auf, dass es zu regelmäßigen Häufungen auf der BonusMalus-Werteskala kommt. Letzter größerer Häufungspunkt mit Anzahl > 100 liegt ungefähr bei Wert 155. => für die Modellierung werden Werte > 160 auf 160 gesetzt und der Skalenbereich in Wertebereiche von jeweils Länge=10 zusammengefasst.Density: ungleichmäßig verteilt ohne log-Transformation, aber relativ gleichmäßig mit log-Transformation => für die Modellierung wird log-skalierter Wert verwendet werden.ClaimAmount und ClaimAmountperClaimNb: log-skalierte Verteilungen ähneln sich sehr. Bzgl. ClaimAmountperClaimNb liegt der Mittelwert bei ~2200 € und der Median bei ~1200 €. Der Maximalwert liegt bei ~4.1 Mio €. 50% der Werte liegen im relativ engen Intervall von ca. 700 € bis 1200 €. Die log-skalierte Verteilung weicht aufgrund der geballten Häufung in diesem Intervall relativ deutlich von einer Normalverteilung ab. Es handel sich ohne Anwendung einer log-Skalierung um eine linkssteile Verteilung, wie man in den untersten Plots ganz gut erkennen kann. Ähnlichkeit zu einer Gammaverteilung ist gegeben und sollte später bei Modellierung berücksichtigt werden.Es wird nun die Schadenhäufigkeit Frequency bzgl. des gesamten Portfolios berechnet und als zusätzliche Spalte der Tabelle hinzugefügt (zwecks späterer Darstellmöglichkeit als Basisvergleichslinie in Plots):
Frequency_Portfolio = df["ClaimNb"].sum() / df["Exposure"].sum()
Frequency_Portfolio_alternativ = np.average(df["Frequency"], weights=df["Exposure"])
# Test ob beide Berechnungsweisen zum selben Ergebnis führen:
assert Frequency_Portfolio == Frequency_Portfolio_alternativ, 'Inkonsistenz in Berechnungsweise!'
print("Schadenhäufigkeit bzgl. gesamtem Portfolio = {0:.1%}".format(Frequency_Portfolio))
#Hinzufügen als neue Spalte zur Tabelle:
df['Frequency_Portfolio'] = Frequency_Portfolio
Schadenhäufigkeit bzgl. gesamtem Portfolio = 10.1%
Somit verursacht ein (hypothetischer) Durchschnittsvertrag aus dem Portfolio ca. alle 10 Jahre einen Schaden.
Es wird nun der Schadensbetrag pro Schadensfall ClaimAmountperClaimNb bzgl. des gesamten Portfolios berechnet und als zusätzliche Spalte der Tabelle hinzugefügt (zwecks späterer Darstellmöglichkeit als Basisvergleichslinie in Plots):
df_temp = df.dropna(subset=["ClaimAmountperClaimNb"])
ClaimAmountperClaimNb_Portfolio = df_temp["ClaimAmount"].sum() / df_temp["ClaimNb"].sum()
ClaimAmountperClaimNb_Portfolio_alternativ = np.average(df_temp["ClaimAmountperClaimNb"], weights=df_temp["ClaimNb"])
# Test ob beide Berechnungsweisen zum selben Ergebnis führen:
assert ClaimAmountperClaimNb_Portfolio == ClaimAmountperClaimNb_Portfolio_alternativ, 'Inkonsistenz in Berechnungsweise!'
print("Schadensbetrag pro Schadensfall bzgl. gesamtem Portfolio = ", round(ClaimAmountperClaimNb_Portfolio, 2))
#Hinzufügen als neue Spalte zur Tabelle:
df['ClaimAmountperClaimNb_Portfolio'] = ClaimAmountperClaimNb_Portfolio
Schadensbetrag pro Schadensfall bzgl. gesamtem Portfolio = 2265.43
Somit verursacht ein (hypothetischer) Durchschnittsvertrag aus dem Portfolio einen Schadensbetrag von 2265.43 € pro Schaden.
Deskriptive Statistik Kennzahlen bzgl. der kategorialen Merkmale:
df[[col for col in df.columns if df[col].dtype in ['object']]].describe()
| VehBrand | VehGas | Area | Region | |
|---|---|---|---|---|
| count | 678013 | 678013 | 678013 | 678013 |
| unique | 11 | 2 | 6 | 21 |
| top | B12 | Regular | C | Centre |
| freq | 166024 | 345877 | 191880 | 160601 |
Histogramm-Plots (kumulativ) um Verteilung der kategorialen Merkmale besser einschätzen zu können:
fig, ax = plt.subplots(2, 2, figsize=(10,7))
sns.histplot(df['Area'], stat='percent', ax=ax[0][0])
sns.histplot(df['VehBrand'], stat='percent', ax=ax[0][1])
sns.histplot(df['VehGas'], stat='percent', ax=ax[1][0])
sns.histplot(df['Region'], stat='percent', ax=ax[1][1])
fig.tight_layout()
Detailbetrachtung: Verteilung der Werte von Region:
df['Region'].value_counts() / df['Region'].count()
Centre 0.236870 Rhone-Alpes 0.125001 Provence-Alpes-Cotes-D'Azur 0.116982 Ile-de-France 0.102935 Bretagne 0.062126 Nord-Pas-de-Calais 0.059402 Pays-de-la-Loire 0.057154 Languedoc-Roussillon 0.052809 Aquitaine 0.046207 Poitou-Charentes 0.028091 Midi-Pyrenees 0.025281 Basse-Normandie 0.016066 Bourgogne 0.015475 Haute-Normandie 0.012956 Picardie 0.011790 Auvergne 0.007798 Limousin 0.006736 Corse 0.006661 Champagne-Ardenne 0.004463 Alsace 0.003245 Franche-Comte 0.001956 Name: Region, dtype: float64
Auffälligkeiten bzgl. der Verteilung der kategorialen Merkmale:
VehBrand: Die 3 Automarken B1, B2, B12 dominieren mit einem Gesamtanteil von ~75%VehGas: Ausgeglichene Aufteilung zwischen Regular und DieselRegion: Region Centre weist mit ~24% die mit Abstand größte Anzahl an Verträgen aufAuf Basis der gewonnenen Erkenntnisse aus der voran gegangenen univariaten Analyse werden nun die Werte einzelner Merkmale angepasst.
Das vereinfacht die nachfolgende multivariate Analyse und macht sie auch aussagekräftiger. Zudem werden diese angepassten Merkmale dann auch als Grundlage für die Modelle verwendet werden - allerdings sind dann noch weitere modellspezifische Anpassungen notwendig. Die Anpassungen werden in einer neuen Tabelle df_prep, auf Basis einer harten Kopie von Tabelle df, vorgenommen.
Zusammengefasst erfolgen folgende Anpassungen:
ClaimNb: Werte > 3 werden auf 3 gesetztExposure: Werte > 1 werden auf 1 gesetztVehAge: Werte > 20 werden auf 20 gesetztDriveAge: Werte > 90 werden auf 90 gesetztBonusMalus: Werte > 160 werden auf 160 gesetzt und der Skalenbereich in Wertebereiche von jeweils Länge=10 zusammengefasst.Density: Werte werden log-skaliert.Density_temp: Temporär benutztes, neues Merkmal für Vereinfachung der multivariaten Analyse, für die Modellierung wird es nicht benötigt. Werte werden log-skaliert und der Skalenbereich in Wertebereiche von jeweils Länge=1 zusammengefasst.Frequency: wird neu berechnet auf Basis angepasster ClaimNb und angepasstem ExposureFür die vor zu nehmenden Merkmals-Anpassungen wird eine Funktion definiert (Code inspiriert aus [5]:
def cutAndBinColumns(df_input):
"""Erzeugt harte Kopie von übergebenem Dataframe df_input und beschneidet und/oder gruppiert dies, um für die
Merkmale die relevanten Wertebereiche zu erhalten.
Wertezuweisung basiert auf Erkenntnissen aus der vorgangenen Daten-Exploration.
Rückgabewert ist das angepasste Dataframe df"""
df = deepcopy(df_input) #Erzeugt harte Kopie von df_input, so dass df_input von Anpassungen unberührt bleibt.
df['ClaimNb'] = df['ClaimNb'].apply(lambda x: 3 if x > 3 else x)
df['Exposure'] = df['Exposure'].apply(lambda x: 1 if x > 1 else x)
df['VehAge'] = df['VehAge'].apply(lambda x: 20 if x > 20 else x)
df['DrivAge'] = df['DrivAge'].apply(lambda x: 90 if x > 90 else x)
df['BonusMalus'] = df['BonusMalus'].apply(lambda x: 160 if x > 160 else round(x / 10) * 10)
df['Density_temp'] = df['Density'].apply(lambda x: round(math.log(x)))
df['Density'] = df['Density'].apply(lambda x: math.log(x))
df['Frequency'] = df['ClaimNb'] / df['Exposure']
return df
Durchführen der Anpassung und speichern als neues Dataframe df_prep:
df_prep = cutAndBinColumns(df)
# Teste Wertebereiche:
assert df_prep['ClaimNb'].max() == 3, "Wertebereich nicht erwartungsgemäß!"
assert df_prep['Exposure'].max() == 1, "Wertebereich nicht erwartungsgemäß!"
assert df_prep['VehAge'].max() == 20, "Wertebereich nicht erwartungsgemäß!"
assert df_prep['DrivAge'].max() == 90, "Wertebereich nicht erwartungsgemäß!"
assert df_prep['BonusMalus'].max() == 160, "Wertebereich nicht erwartungsgemäß!"
assert df_prep['Density'].max() < 20, "Wertebereich nicht erwartungsgemäß!"
df_prep
| IDpol | ClaimNb | Exposure | VehPower | VehAge | DrivAge | BonusMalus | VehBrand | VehGas | Area | Density | Region | ClaimAmount | ClaimAmountperClaimNb | Frequency | Frequency_Portfolio | ClaimAmountperClaimNb_Portfolio | Density_temp | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 0.10000 | 5.0 | 0.0 | 55.0 | 50 | B12 | Regular | D | 7.104144 | Rhone-Alpes | NaN | NaN | 10.000000 | 0.100703 | 2265.426981 | 7 |
| 1 | 3.0 | 1.0 | 0.77000 | 5.0 | 0.0 | 55.0 | 50 | B12 | Regular | D | 7.104144 | Rhone-Alpes | NaN | NaN | 1.298701 | 0.100703 | 2265.426981 | 7 |
| 2 | 5.0 | 1.0 | 0.75000 | 6.0 | 2.0 | 52.0 | 50 | B12 | Diesel | B | 3.988984 | Picardie | NaN | NaN | 1.333333 | 0.100703 | 2265.426981 | 4 |
| 3 | 10.0 | 1.0 | 0.09000 | 7.0 | 0.0 | 46.0 | 50 | B12 | Diesel | B | 4.330733 | Aquitaine | NaN | NaN | 11.111111 | 0.100703 | 2265.426981 | 4 |
| 4 | 11.0 | 1.0 | 0.84000 | 7.0 | 0.0 | 46.0 | 50 | B12 | Diesel | B | 4.330733 | Aquitaine | NaN | NaN | 1.190476 | 0.100703 | 2265.426981 | 4 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 678008 | 6114326.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 54.0 | 50 | B12 | Regular | E | 8.106816 | Provence-Alpes-Cotes-D'Azur | NaN | NaN | 0.000000 | 0.100703 | 2265.426981 | 8 |
| 678009 | 6114327.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 41.0 | 100 | B12 | Regular | E | 9.195227 | Ile-de-France | NaN | NaN | 0.000000 | 0.100703 | 2265.426981 | 9 |
| 678010 | 6114328.0 | 0.0 | 0.00274 | 6.0 | 2.0 | 45.0 | 50 | B12 | Diesel | D | 7.187657 | Rhone-Alpes | NaN | NaN | 0.000000 | 0.100703 | 2265.426981 | 7 |
| 678011 | 6114329.0 | 0.0 | 0.00274 | 4.0 | 0.0 | 60.0 | 50 | B12 | Regular | B | 4.553877 | Bourgogne | NaN | NaN | 0.000000 | 0.100703 | 2265.426981 | 5 |
| 678012 | 6114330.0 | 0.0 | 0.00274 | 7.0 | 6.0 | 29.0 | 50 | B12 | Diesel | B | 4.174387 | Aquitaine | NaN | NaN | 0.000000 | 0.100703 | 2265.426981 | 4 |
678013 rows × 18 columns
Es wird nun eine Hilfsfunktionen definiert, welche das Plotten von marginalen Schadenhäufigkeiten im Folgenden vereinfacht. Sie enthält auch Features, die erst später in der Modellierung verwendet werden (Code inspiriert aus [5]):
def plotFrequenciesAndExposures(df, columnsToPlot, predictionValues = False):
"""Plottet die gemessenen marginalen Schadenhäufigkeiten und Exposures bzgl. aller übergebenen Merkmale (columnsToPlot).
Optional kann auch predictionValues = True gesetzt werden, dadurch werden auch die von den Modellen
prognostizierten Werte angezeigt."""
fig, ax = plt.subplots(len(columnsToPlot), 2, figsize=(10, len(columnsToPlot) * 5))
i = 0 #Index für subplots
#Erstelle Dictionary mit zu aggregierenden Merkmalen und der zu benutzenden Aggregationsmethode sowie Liste
# an Merkmalen für die Linienplot gezeichnet werden soll (Hinweis: bzgl. 'Frequency_Portfolio' sind die Werte
# alle gleich, d.h. bildet die Aggregatfunktion min genau wieder diesen Wert ab):
dict_agg = {'ClaimNb':sum, 'Exposure':sum, 'Frequency_Portfolio':'min'}
list_lp = []
#Falls predictionValues = True, hole alle vorhergesagten Zielgrößen aus übergebenem dataframe und füge diese dem
#Dictionary und der Liste hinzu:
if predictionValues == True:
dict_agg.update({pred: 'mean' for pred in df_test_Freq.columns if 'Frequency_pred' in pred})
list_lp += [pred for pred in df_test_Freq.columns if 'Frequency_pred' in pred]
# Für jedes übergebene Merkmal wird diese Schleife durchlaufen, die jeweils 2 Plots erstellt:
for col in columnsToPlot:
# Gruppieren bzgl. Werten des übergebenen Merkmals
df_pivot = df.groupby(by=[col]).agg(dict_agg).reset_index()
# Index auf col Spalte setzen, vereinfacht plotten
df_pivot.set_index(col, inplace=True)
# Gemessene Schadenhäufigkeit auf Basis der Schadenanzahl und Versicherungszeit in der Gruppe berechnen:
df_pivot['Frequency'] = df_pivot['ClaimNb'] / df_pivot['Exposure']
# Linienplot bzgl. Frequency
_ = sns.lineplot(data=df_pivot['Frequency'], label='Frequency', color='black', ax=ax[i][0])
# Standardabweichung bzgl. der wahren Frequency berechnen unter Annahme eines Poisson-Models (aus [3], S. 17):
df_pivot['Standarddeviation'] = np.sqrt(df_pivot['ClaimNb'])/ df_pivot['Exposure']
#Konfidenzintervall mit 2-facher Standardabweichung bzgl. gemessener Frequency berechnen als Linienplots zeichnen
df_pivot['CI_LowerBound'] = df_pivot['Frequency'] - 2 * df_pivot['Standarddeviation']
df_pivot['CI_UpperBound'] = df_pivot['Frequency'] + 2 * df_pivot['Standarddeviation']
# Konfidenzintervall mit 2-facher Standardabweichung um Linienplots zeichen:
ax[i][0].fill_between(df_pivot.index, df_pivot['CI_LowerBound'], df_pivot['CI_UpperBound'],
alpha=0.25, color='grey')
if predictionValues == True:
# Linienplots bzgl. vorhergesagter Frequency des Modells
#_ = sns.lineplot(data=df_pivot[list_lp[0]], markers=True, ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[0]], label=list_lp[0], color='green', markers=False, ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[1]], label=list_lp[1], color='red', marker='o', ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[2]], label=list_lp[2], color='blue', marker='*', ax=ax[i][0])
else:
# Linienplot bzgl. Frequency_Portfolio
_ = sns.lineplot(data=df_pivot['Frequency_Portfolio'], label='Frequency_Portfolio', color='green', ax=ax[i][0])
# Barplot der kumulierten Versicherungszeit bzgl. des jeweiligen Werts des übergebenen Merkmals
_ = sns.barplot(data=df_pivot, x=df_pivot.index, y='Exposure', ax=ax[i][1])
#Skalierung der Achsen
ax[i][0].set_ylim(0.05, 0.5)
ax[i][0].set_yticks(ticks = [i/100 for i in range(5, 55, 5)])
#ax[i][1].set_ylim(0, 30000)
#ax[i][1].set_yticks(ticks = [i for i in range(0, 31000, 5000)])
i += 1
fig.tight_layout()
Die Marginalen Schadenhäufigkeiten bzgl. der einzelnen Merkmale können interessante erste Hinweise bzgl. dessen Beitrag zur Schadenhäufigkeit liefern. Deshalb wird im Folgenden die marginale Schadenhäufigkeit bzgl. der einzelnen Merkmale geplottet mit zusätzlicher Darstellung eines Konfidenzintervalls auf Basis der 2-fachen Standardabweichung unter einer Possion-Verteilungs-Annahme der Schadenhäufigkeit. Zusätzlich wird die Schadenhäufigkeit bzgl. des gesamten Portfolios als Vergleichsbasis mit dargestellt. Zudem wird das Exposure (summierte Versicherungszeit) bzgl. des jeweiligen Werteabschnitts als Balkendiagramm rechts daneben dargestellt - niedrige Werte sind nämlich mit höherer Schätzunsicherheit bzgl. des wahren Schadenhäufigkeitsparameters der Verteilung verbunden. Bzgl. aller Merkmale sind die Schadenhäufigkeits-Werte auf der y-Achse gleich skaliert um einen direkten visuellen Vergleich zu ermöglichen.
plotFrequenciesAndExposures(df_prep, ['VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'VehBrand',
'VehGas', 'Area', 'Density_temp', 'Region'], predictionValues = False)
Analyse der Plots bzgl. der marginalen Schadenhäufigkeiten der einzelnen Merkmale:
VehPower: Keiner klarer Zusammenhang bzgl. Motorleistung und Schadenhäufigkeit erkennbar. Merkwürdig ist der Abfall bei Wert 8 und das Plateau bei 9 und 10. Werte >12 sind mit großer Unsicherheit behaftet auf Grund geringen Exposures. Insgesamt ist der Einfluss des Merkmals auf die Schadenhäufigkeit als gering zu bewerten und braucht bei Modellierung nicht unbedingt berücksichtigt werden.VehAge: Ältere Autos weisen eine zunehmend geringere Schadenhäufigkeit auf. Merkwürdig wirkt die hohe Schadenhäufigkeit für Neuwagen von über 30%. Das deutet auf eine eventuell verborgene Sonderheit bzgl. der Neuwagen in dem Datensatz hin. Insgesamt ist Einfluss des Merkmals auf die Schadenhäufigkeit als relevant zu bewerten und sollte bei der Modellierung berücksichtigt werden.DrivAge: Auffällig deutlich höhere Schadenhäufigkeit für junge Fahrer bis ca. 26 Jahre. Werte ab 80 Jahre auf Grund geringen Exposures nicht aussagekräftig. Insgesamt ist Einfluss des Merkmals auf die Schadenhäufigkeit als relevant zu bewerten und sollte bei der Modellierung berücksichtigt werden.BonusMalus: Klarer Zusammenhang zwischen BonusMalus-Wert und Schadenhäufigkeit erkennbar. Für Wert 50 liegt Schadenhäufigkeit bei nur ~8%, während sie ab Wert 100 auf >20% ansteigt. Das BonusMalus-Scoring scheint somit seinen Zweck zu erfüllen. Insgesamt ist Einfluss des Merkmals auf die Schadenhäufigkeit als sehr relevant zu bewerten und sollte unbedingt bei der Modellierung berücksichtigt werden.VehBrand: Die Automarken B12 & B14 stechen hervor mit einer relativ hohen bzw. niedrigen Schadenhäufigkeit hervor. Das ist merkwürdig und deutet auf eine eventuell verborgene Sonderheit bzgl. der versicherten Fahrzeuge dieser Marke in diesem Datensatz hin. Ansonsten ist der Einfluss des Merkmals auf die Schadenhäufigkeit als gering zu bewerten, sollte aber wegen der Besonderheit bzgl. der Automarken der B12 und B14 bei Modellierung berücksichtigt werden.VehGas: Kein relevanter Unterschied zwischen Diesel und Regular erkennbar. Insgesamt ist der marginale Einfluss des Merkmals auf die Schadenhäufigkeit als gering zu bewerten. Wird dennoch bei Modellierung berücksichtigt, da prinzipiell auch kompliziertere Interaktion mit anderen Merkmalen möglich ist.Area und Density: Jeweils klarer positiver Zusammenhang mit Schadenhäufigkeit erkennbar. Zudem sind die Verläufe der Linienplots sehr ähnlich bzgl. Area und Density. Da der Code von Area gemäß Definition anhand der Bevölkerungsdichte festgelegt wurde, ist das nicht verwunderlich => wird weiter unten noch untersucht werden. Der positive Zusammenhang von Schadenhäufigkeit mit der Bevölkerungsdichte ist nicht verwunderlich, da vermutlich höhere Bevölkerungsdichte mit höherem Verkehrsaufkommen und damit höherer Unfallgefahr korreliert. Insgesamt ist Einfluss des Merkmals Density auf die Schadenhäufigkeit als relevant zu bewerten und sollte bei der Modellierung berücksichtigt werden. Auf das Merkmal Area kann bei der Modellierung tendenziell verzichtet werden, da es redundant zu Density ist (wird noch näher untersucht).Es wird nun eine Hilfsfunktionen definiert, welche das Plotten von marginalen Schadensbeträgen im Folgenden vereinfacht. Sie enthält auch Features, die erst später in der Modellierung verwendet werden:
def plotClaimAmountperClaimNb(df, columnsToPlot, predictionValues = False):
"""Plottet die gemessenen marginalen Schadenbeträge bzgl. aller übergebenen Merkmale (columnsToPlot).
Optional kann auch predictionValues = True gesetzt werden, dadurch werden auch die von den Modellen
prognostizierten Werte angezeigt."""
fig, ax = plt.subplots(len(columnsToPlot), 2, figsize=(10, len(columnsToPlot) * 5))
i = 0 #Index für subplots
#Erstelle Dictionary mit zu aggregierenden Merkmalen und der zu benutzenden Aggregationsmethode sowie Liste
# an Merkmalen für die Linienplot gezeichnet werden soll (Hinweis: bzgl. 'ClaimAmountperClaimNb_Portfolio' sind
# die Werte alle gleich, d.h. bildet die Aggregatfunktion min genau wieder diesen Wert ab):
dict_agg = {'ClaimNb':sum, 'ClaimAmount':sum, 'ClaimAmountperClaimNb_Portfolio':'min'}
list_lp = []
#Falls predictionValues = True, hole alle vorhergesagten Zielgrößen aus übergebenem dataframe und füge diese dem
# Dictionary und der Liste hinzu:
if predictionValues == True:
dict_agg.update({pred: 'mean' for pred in df_test_Claim.columns if 'ClaimAmountperClaimNb_pred' in pred})
list_lp += [pred for pred in df_test_Claim.columns if 'ClaimAmountperClaimNb_pred' in pred]
# Für jedes übergebene Merkmal wird diese Schleife durchlaufen, die jeweils 2 Plots erstellt:
for col in columnsToPlot:
# Einschränkung von df auf die Zeilen für die ClaimAmount nicht leer ist
df = df.dropna(subset=['ClaimAmount'])
# Gruppieren bzgl. Werten des übergebenen Merkmals
df_pivot = df.groupby(by=[col]).agg(dict_agg).reset_index()
# Index auf col Spalte setzen, vereinfacht plotten
df_pivot.set_index(col, inplace=True)
# Gemessenen Schadenbetrag pro Schadensfall auf Basis der Schadensbeträge und Schadenanzahl in der Gruppe berechnen:
df_pivot['ClaimAmountperClaimNb'] = df_pivot['ClaimAmount'] / df_pivot['ClaimNb']
# Linienplot bzgl.'ClaimAmountperClaimNb' mit 95%-Konfidenzintervall gemäß Bootstraping
_ = sns.lineplot(data=df, x=col, y='ClaimAmountperClaimNb', label='ClaimAmountperClaimNb',
color='black', ci=95, ax=ax[i][0])
if predictionValues == True:
# Linienplots bzgl. vorhergesagter Schadenbetrag pro Schadensfall des Modells
#_ = sns.lineplot(data=df_pivot[list_lp], markers=True, ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[0]], label=list_lp[0], color='green', markers=False, ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[1]], label=list_lp[1], color='red', marker='o', ax=ax[i][0])
_ = sns.lineplot(data=df_pivot[list_lp[2]], label=list_lp[2], color='blue', marker='*', ax=ax[i][0])
else:
# Linienplot bzgl. ClaimAmountperClaimNb_Portfolio
_ = sns.lineplot(data=df_pivot['ClaimAmountperClaimNb_Portfolio'], label='ClaimAmountperClaimNb_Portfolio',
color='green', ax=ax[i][0])
# Barplot der kumulierten Schadensanzahl bzgl. des jeweiligen Werts des übergebenen Merkmals
_ = sns.barplot(data=df_pivot, x=df_pivot.index, y='ClaimNb', ax=ax[i][1])
#Skalierung der Achsen
ax[i][0].set_ylim(0, 10000)
ax[i][0].set_yticks(ticks = [i for i in range(0, 11000, 1000)])
#ax[i][1].set_ylim(0, 30000)
#ax[i][1].set_yticks(ticks = [i for i in range(0, 31000, 5000)])
i += 1
fig.tight_layout()
Die marginalen Schadensbeträge bzgl. der einzelnen Merkmale können interessante erste Hinweise bzgl. dessen Beitrag zum Schadensbetrag liefern. Deshalb wird im Folgenden der marginale Schadensbetrag bzgl. der einzelnen Merkmale geplottet mit zusätzlicher Darstellung eines 95%-Konfidenzintervalls auf Basis von Bootstrapping. Zusätzlich wird der Schadensbetrag bzgl. des gesamten Portfolios als Vergleichsbasis mit dargestellt. Zudem wird die summierte Schadensanzahl des jeweiligen Werteabschnitts als Balkendiagramm rechts daneben dargestellt - niedrige Werte sind nämlich mit höherer Schätzunsicherheit bzgl. des wahren Schadensbetragsparameters der Verteilung verbunden. Bzgl. aller Merkmale sind die Schadensbetrags-Werte auf der y-Achse gleich skaliert um einen direkten visuellen Vergleich zu ermöglichen.
plotClaimAmountperClaimNb(df_prep, ['VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'VehBrand',
'VehGas', 'Area', 'Density_temp', 'Region'], predictionValues = False)
Analyse der Plots bzgl. der marginalen Schadenbeträge der einzelnen Merkmale:
#Plausilibitätschecks:
# Einschränkung von df auf der Zeilen für die ClaimAmount nicht leer ist
df_temp = df_prep.dropna(subset=['ClaimAmount'])
# Gruppieren bzgl. Werten des übergebenen Merkmals
df_pivot = df_temp.groupby(by='Density_temp').agg({'IDpol': 'count', 'ClaimNb':sum, 'ClaimAmount':sum}).reset_index()
df_pivot['ClaimAmountperClaimNb'] = df_pivot['ClaimAmount'] / df_pivot['ClaimNb']
df_pivot
| Density_temp | IDpol | ClaimNb | ClaimAmount | ClaimAmountperClaimNb | |
|---|---|---|---|---|---|
| 0 | 1 | 21 | 22.0 | 48010.32 | 2182.287273 |
| 1 | 2 | 419 | 434.0 | 982056.22 | 2262.802350 |
| 2 | 3 | 1628 | 1680.0 | 3095201.40 | 1842.381786 |
| 3 | 4 | 3190 | 3341.0 | 7588518.43 | 2271.331467 |
| 4 | 5 | 4229 | 4452.0 | 13176911.77 | 2959.773533 |
| 5 | 6 | 4095 | 4315.0 | 8700801.22 | 2016.408162 |
| 6 | 7 | 4644 | 4930.0 | 11661607.80 | 2365.437688 |
| 7 | 8 | 4666 | 5003.0 | 10163831.10 | 2031.547292 |
| 8 | 9 | 1435 | 1543.0 | 3447661.79 | 2234.388717 |
| 9 | 10 | 617 | 673.0 | 1044616.45 | 1552.178975 |
22*2182.287273
48010.320005999994
In diesem Abschnitt wird auf Zusammenhänge zwischen den einzelnen Merkmalen untersucht. Dabei wird der Zusammenhang zwischen den numerischen Merkmalen mittels Berechnung der Korrelation untersucht. Der paarweise Zusammenhang zwischen den restlichen Merkmalen wird mittels Darstellung von Heatmaps hinsichtlich des aggregierten Versicherungszeitraums Exposure untersucht.
Um den Zusammenhang zwischen den numerischen Merkmalen zu untersuchen, wird die Pearson und Spearman Korrelation angewendet:
fig, ax = plt.subplots(1, 2, figsize=(10,5))
ax[0].set_title('Pearson Korrelation')
ax[1].set_title('Spearman Korrelation')
df_corr_pear = df_prep[['VehPower','VehAge','DrivAge','BonusMalus','Density']]
mask_ut = np.triu(np.ones(df_corr_pear.corr().shape)).astype(bool)
sns.heatmap(df_corr_pear.corr(), mask=mask_ut, annot=True, ax=ax[0])
df_corr_spear = df_prep[['VehPower','VehAge','DrivAge','BonusMalus','Density']]
mask_ut = np.triu(np.ones(df_corr_spear.corr(method='spearman').shape)).astype(bool)
sns.heatmap(df_corr_spear.corr(method='spearman'), mask=mask_ut, annot=True, ax=ax[1])
fig.tight_layout()
Pearson und Spearman Korrelatonsmaß messen die Zusammenhänge zwischen den Merkmalen sehr ähnlich. Auffälligkeiten:
BonusMalus und DrivAge: deutlich negativer Zusammenhang zwischen den beiden Merkmalen: je jünger der Fahrer desto höher ist das BonusMalus-Level. Dieser Zusammenhang ist angesichts der bereits zuvor beobachteten höheren Schadenhäufigkeit bei jüngeren Fahrern nicht überraschend.BonusMalus und Density: schwach positiver Zusammenhang zwischen den beiden Merkmalen: Hier könnte wieder das bereits zuvor beobachtete Muster eine Rolle spielen, dass höhere Bevölkerungsdichte mit höherer Schadenhäufigkeit korreliert.VehAge und Density: schwach negativer Zusammenhang zwischen den beiden Merkmalen: Fahren auf dem Land eher ältere Autos als in der Stadt?Nun wird der paarweise Zusammenhang zwischen den restlichen Merkmalen mittels Darstellung von Heatmaps hinsichtlich des aggregierten Versicherungszeitraums Exposure untersucht:
# Definition notwendiger Spalten
columns_cat = ['Area', 'VehBrand', 'VehGas', 'Region']
columns_cat_used = []
columns_num = ['VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'Density_temp']
columns_all = columns_cat + columns_num
# Konfiguration Darstellung
fig, ax = plt.subplots(13, 2, figsize=(10, 13 * 4))
i, j = (0, 0)
# Schleife zur Darstellung aller kategorialen Merkmalen gegenüber restlichen Merkmalen hinsichtlich Exposure-Summe
for col_cat in columns_cat:
columns_cat_used.append(col_cat)
for col_all in [col for col in columns_all if col not in columns_cat_used]:
sns.heatmap(pd.pivot_table(df_prep, index=col_cat, columns=col_all, values='Exposure', aggfunc=np.sum),
ax=ax[i][j])
if j < 1:
j += 1
else:
i += 1
j = 0
fig.tight_layout()
Ergebnisse der paarweisen Untersuchung der Merkmale auf Zusammenhänge:
Area und Density lässt sich gut nachvollziehen, dass die Kategorien A bis F von Area bzgl. Density (Bevölkerungsdichte) kalibriert worden sind. Somit sollte für die Modellierung nur eine der beiden Variablen berücksichtigt werden, da Multikollinearität in den unabhängigen Variablen eines Modells zur ungenaueren Schätzung der Regressionskoeffizienten führen kann.VehBrand B12 weist außergewöhnlich viele relativ neue versicherte Wagen im Alter von 1-3 Jahren auf.Nun wird mit der Modellierung begonnen um Schadenwahrscheinlichkeit und Schadenaufwand für einen Kunden mit gegebenen Risikomerkmalen prognostizieren zu können. Sowohl für die Modellierung der Schadenwahrscheinlichkeit als auch für den Schadenaufwand werden jeweils folgende Modelle definiert und verwendet mit teils unterschiedlicher Konfiguration bzgl. Schadenwahrscheinlichkeit und Schadenaufwand:
Dabei wird für jedes Modell eine individuelle Datenverarbeitungspipeline gebaut, da je nach Modelltyp unterschiedliche Datenaufbereitungen nötig sind. Die Daten werden dann jeweils bzgl. der Zielgrößen Schadenwahrscheinlichkeit und Schadenaufwand in Trainings- und Testdaten aufgespalten. Die Modelle werden dann jeweils auf Basis der Trainingsdaten trainiert. Mit dem jeweils trainierten Modell werden anschließend die Zielgrößen-Werte vorhergesagt - sowohl bzgl. der Trainings- als auch bzgl. der Testdaten. Auf Basis der gemessenen ggü. den vorhergesagten Werten wird je Modell und pro Trainings- und Testdatensatz jeweils eine Scoring-Wert basierend auf der passenden Performance-Metrik berechnet. U.a. anhand der Scoring-Werte kann die Modellperformance dann miteinander verglichen werden. In den danach folgenden Abschnitten finden dann zusätzlich visuelle Vergleiche der Modelle anhand der prognostizierten marginalen Werte bzgl. der Zielgrößen statt. Zudem findet ein visuelle Kontrolle der Modellkalibrierung statt, indem die prognostizierten Werte in geordneter Reihenfolge den gemessenen Werte gegenüber gestellt werden.
Nachfolgende Variablen und Funktionen werden später für die Modellauswertung benötigt.
# Definition der Dictionaries 'scoringResults_Frequency' und 'scoringResults_ClaimAmountperClaimNb' zwecks Speicherung
# von Modellergebnissen
scoringResults_Frequency = {}
scoringResults_ClaimAmountperClaimNb = {}
def predictAndSaveResults(modelName, model, targetName='Frequency'):
"""Prognostiziert Zielgröße mit übergebenem Namen 'targetName' (='Frequency' oder ='ClaimAmountperClaimNb')
auf Basis des übergebenen, gefitteten Modells 'model' mit Name 'modelName' sowohl für die
zugehörigen Trainings- als auch Testdaten.
Fügt prognostizierte Zielgrößen-Werte den Dataframes jeweiligen Trainings- als auch Testdataframes hinzu.
Fügt Scoringergebnisse dem Dictionary 'scoringResults_Frequency' bzw. 'scoringResults_ClaimAmountperClaimNb' hinzu."""
if targetName == 'Frequency':
# Berechne die prognostizierte Werten des Modells bzgl. df_train_Freq und df_test_Freq
df_train_Freq[targetName + '_pred_' + modelName] = model.predict(df_train_Freq)
df_test_Freq[targetName + '_pred_' + modelName] = model.predict(df_test_Freq)
#Berechnen der mittleren Poisson-Abweichung bzgl. 'Frequency' für df_train_Freq und df_test_Freq
scoringResults_Frequency[modelName + '_meanPoissonDeviance_df_train_Freq'] = round(
mean_poisson_deviance(df_train_Freq["Frequency"],
df_train_Freq['Frequency_pred_' + modelName],
sample_weight=df_train_Freq["Exposure"]), 4)
scoringResults_Frequency[modelName + '_meanPoissonDeviance_df_test_Freq'] = round(
mean_poisson_deviance(df_test_Freq["Frequency"],
df_test_Freq['Frequency_pred_' + modelName],
sample_weight=df_test_Freq["Exposure"]), 4)
result_table = pd.DataFrame(scoringResults_Frequency.items(), columns=['Model_Metric_Data_Target', 'Value of Metric'])
else:
# Berechne die prognostizierte Werten des Modells bzgl. df_train_Claim und df_test_Claim
df_train_Claim[targetName + '_pred_' + modelName] = model.predict(df_train_Claim)
df_test_Claim[targetName + '_pred_' + modelName] = model.predict(df_test_Claim)
#Berechnen der mittleren Gamma-Abweichung bzgl. 'ClaimAmountperClaimNb' für df_train_Claim und df_test_Claim
scoringResults_ClaimAmountperClaimNb[modelName + '_meanGammaDeviance_df_train_Claim'] = round(
mean_gamma_deviance(df_train_Claim["ClaimAmountperClaimNb"],
df_train_Claim['ClaimAmountperClaimNb_pred_' + modelName],
sample_weight=df_train_Claim["ClaimNb"]), 4)
scoringResults_ClaimAmountperClaimNb[modelName + '_meanGammaDeviance_df_test_Claim'] = round(
mean_gamma_deviance(df_test_Claim["ClaimAmountperClaimNb"],
df_test_Claim['ClaimAmountperClaimNb_pred_' + modelName],
sample_weight=df_test_Claim["ClaimNb"]), 4)
result_table = pd.DataFrame(scoringResults_ClaimAmountperClaimNb.items(), columns=['Model_Metric_Data_Target', 'Value of Metric'])
return result_table
Das Dummy-Regressor Modell ist ein triviales Modell, welches unabhängig von sämtlichen Merkmalen konstant den gewichteten Trainingsdaten-Mittelwert bzgl. der Zielgröße ausgibt. Es dient hinsichtlich der Performance-Metriken als Basisvergleichslinie für die anderen Modelle. D.h. so lässt sich feststellen, ob das GLM- oder XGBoost-Modell überhaupt einen Mehrwert in der Prognose ggü. solch einem simplen Prognosemodell ergeben.
Da es sich um ein triviales Modell handelt, bei dem die Merkmale keine Rolle für den Prognosewert spielen, ist keine Datenvorverarbeitung für das Modell notwendig.
Es ist keine Definition einer Pipeline notwendig, da das Dummy-Regressor Modell keinen Datenvorverabeitungsprozess benötigt - somit wird das Dummy-Regressor Modell direkt definiert. Zu beachtende Unterschiede bzgl. Dummy-Regressor Modell für Schadenwahrscheinlichkeit ggü. Schadenaufwand sind:
Frequency wird später beim Training mit der Variable Exposure gewichtetClaimAmountperClaimNb wird später beim Training mit der Variable ClaimNb gewichtet#Dummy-Regressor bzgl. Frequency und ClaimAmountperClaimNb
dummy_Frequency = DummyRegressor(strategy='mean')
dummy_ClaimAmountperClaimNb = DummyRegressor(strategy='mean')
Bzgl. der Prognose der Schadenwahrscheinlichkeit Frequency wird ein Generalized linear model (GLM) unter Poisson-Verteilungsannahme mit log-Link Funktion verwendet, da dies ein gängiges Modell für die Prognose von erwarteten Schadenhäufigkeiten unter gegebenen Risikomerkmalen im Sachversicherungsbereich ist (vgl. [2], Abstract).
Bzgl. der Prognose des Schadenaufwands pro Schadensfall ClaimAmountperClaimNb wird ein Generalized linear model (GLM) unter Gamma-Verteilungsannahme mit log-Link Funktion verwendet, da dies ein gängiges Modell für die Prognose von erwartetem Schadenaufwand pro Schadensfall unter gegebenen Risikomerkmalen im Sachversicherungsbereich ist (vgl. https://medium.com/swlh/modeling-insurance-claim-severity-b449ac426c23).
Der Code bzgl. Datenvorverarbeitung und Pipeline ist inspiriert aus [4].
Sowohl bzgl. dem GLM für Schadenwahrscheinlichkeit als auch bzgl. dem für Schadenaufwand wird ein Datenvorverarbeitungsprozess angewandt. Dazu wird eine Pipeline definiert, die bzgl. der Merkmale spezifsche Daten-Transformationen durchführt. Dabei werden die Transformationen auf Basis der Erkenntnisse aus der voran gegangenen Daten-Erkundung im Zusammenhang mit den benötigten Merkmals-Eigenschaften für das GLM definiert. Folgende GLM spezifische Besonderheiten müssen beachtet werden:
Erläuterung der Transformationen der einzelnen Variablen auf Basis der Erkenntnisse aus der voran gegangenen Daten-Erkundung für beide GLMs (unterscheiden sich nur hinsichtlich der Transformation von BonusMalus - ansonsten gleich):
BonusMalus: bzgl. Schadenwahrscheinlichkeit keine Transformation, da (steigend) monotoner Zusammenhang mit der marginalen Schadenhäufigkeit. Bzgl. Schadenaufwand one-hot-encoding da kein klarer Zusammenhang erkennbar und somit ein nicht-monotoner Zusammenhang bestehen könnte.VehAge und DrivAge: Diskretisierierung in jeweils 10 Teile und dann One-hot-encoding, da sie gemäß der erfolgten Daten-Erkundung einen nicht-monotonen Zusammenhang mit der marginalen Schadenhäufigkeit aufweisenDensity: Log-Skalierung. Durch Log-Skalierung wird Wertebereich deutlich eingegrenzt - bei zu großem Wertebereich bestünde nämlich Gefahr, dass dieses Merkmal ggü. den anderen Merkmalen zu stark dominiert beim Fitten des Modells.VehPower: One-hot-encoding, da VehPower gemäß der erfolgten Daten-Erkundung einen nicht-monotonen Zusammenhang mit der marginalen Schadenhäufigkeit aufweistVehBrand, VehGas, Region: One-hot-encoding, da es sich um kategoriale Variablen handelt# Preprocessor für das GLM bzgl. Frequency
glm_preprocessor_Frequency = ColumnTransformer(
[
("passthrough_numeric", "passthrough", ["BonusMalus", "Density"]),
("binned_onehot_numeric", KBinsDiscretizer(n_bins=10, subsample=200000), ["VehAge", "DrivAge"]),
("onehot_categorical", OneHotEncoder(), ["VehBrand", "VehPower", "VehGas", "Region"]),
],
remainder="drop"
)
# Preprocessor für das GLM bzgl. ClaimAmountperClaimNb
glm_preprocessor_ClaimAmountperClaimNb = ColumnTransformer(
[
("passthrough_numeric", "passthrough", ["Density"]),
("binned_onehot_numeric", KBinsDiscretizer(n_bins=10, subsample=200000), ["VehAge", "DrivAge"]),
("onehot_numeric", OneHotEncoder(), ["BonusMalus"]),
("onehot_categorical", OneHotEncoder(), ["VehBrand", "VehPower", "VehGas", "Region"]),
],
remainder="drop"
)
# Test ob Größe des transformierten Dataframes der Erwartung entspricht:
df_prep_array_Freq_raw = glm_preprocessor_Frequency.fit_transform(df_prep)
df_prep_array_Freq = pd.DataFrame(df_prep_array_Freq_raw.toarray())
assert df_prep_array_Freq.shape[1] == 2+2*10+11+12+2+21, "Größe des Arrays nicht erwartungsgemäß!"
df_prep_array_Claim_raw = glm_preprocessor_ClaimAmountperClaimNb.fit_transform(df_prep)
df_prep_array_Claim = pd.DataFrame(df_prep_array_Claim_raw.toarray())
assert df_prep_array_Claim.shape[1] == 1+2*10+df_prep.BonusMalus.nunique()+11+12+2+21, "Größe des Arrays nicht erwartungsgemäß!"
Definition der jeweiligen Pipeline aus Datenvorverarbeitung und GLM-Regressor
Anmerkungen zum Code:
Frequency wird später beim Training mit der Variable Exposure gewichtetClaimAmountperClaimNb wird später beim Training mit der Variable ClaimNb gewichtet# Pipeline für Frequency
glm_poisson = Pipeline(
[
("preprocessor", glm_preprocessor_Frequency),
("regressor", PoissonRegressor(alpha=1e-12, max_iter=300)),
]
)
# Pipeline für ClaimAmountperClaimNb
glm_gamma = Pipeline(
[
("preprocessor", glm_preprocessor_ClaimAmountperClaimNb),
("regressor", GammaRegressor(alpha=1e-12, max_iter=300)),
]
)
Bzgl. der Prognose der Schadenwahrscheinlichkeit Frequency und bzgl. der Prognose des Schadenaufwands pro Schadensfall ClaimAmountperClaimNbwird nun ein XGBoost-Modell definiert und verwendet, da dies seit jüngerer Zeit ein sehr erfolgreich eingesetztes Machine-Learning-Modell ist (vgl. https://machinelearningmastery.com/xgboost-for-regression/)
Analog zu den GLMs wird bzgl. Prognose der Schadenwahrscheinlichkeit ein XGBoost-Modell mit Kostenfunktion auf Basis von Poissonverteilung und bzgl. der Prognose des Schadenaufwands pro Schadensfall ein XGBoost-Modell mit Kostenfunktion auf Basis von Gammaverteilung verwendet. Das XGBoost-Modell in der gewählten Konfiguration basiert auf dem Optimieren von auf einander aufbauenden Entscheidungsbäumen. Dabei versucht der jüngste Entscheidungsbaum die Residuenabweichung aus der gemeinsamen Vorhersage der der anderen Entscheidungsbäume weiter zu optimieren.
Sowohl bzgl. dem XGBoost-Modell für Schadenwahrscheinlichkeit als auch bzgl. dem für Schadenaufwand wird ein Datenvorverarbeitungsprozess angewandt. Dazu wird eine Pipeline definiert, die bzgl. der Merkmale spezifsche Daten-Transformationen durchführt. Dabei werden die Transformationen auf Basis der Erkenntnisse aus der voran gegangenen Daten-Erkundung im Zusammenhang mit den benötigten Merkmals-Eigenschaften für das XGBoost-Modell definiert. Folgende XGBoost-Modell spezifische Besonderheiten müssen beachtet werden:
Erläuterung der Transformationen der einzelnen Variablen auf Basis der Erkenntnisse aus der voran gegangenen Daten-Erkundung für beide XG-Boost-Modelle:
xgboost_preprocessor_Frequency = ColumnTransformer(
[
("passthrough_numeric", "passthrough", ["BonusMalus", "Density", "VehAge", "DrivAge", "VehPower"]),
("ordinal_categorical", OrdinalEncoder(), ["VehBrand", "VehGas", "Region"]),
],
remainder="drop"
)
xgboost_preprocessor_ClaimAmountperClaimNb = ColumnTransformer(
[
("passthrough_numeric", "passthrough", ["BonusMalus", "Density", "VehAge", "DrivAge", "VehPower"]),
("onehot_categorical", OneHotEncoder(), ["VehBrand", "VehGas", "Region"]),
],
remainder="drop"
)
# Test ob Größe des transformierten Dataframes der Erwartung entspricht:
df_prep_array_Claim_raw = xgboost_preprocessor_ClaimAmountperClaimNb.fit_transform(df_prep)
df_prep_array_Claim = pd.DataFrame(df_prep_array_Claim_raw.toarray())
assert df_prep_array_Claim.shape[1] == (5 +
df_prep.VehBrand.nunique() +
df_prep.VehGas.nunique() +
df_prep.Region.nunique()
), "Größe des Arrays nicht erwartungsgemäß!"
Definition der jeweiligen Pipeline aus Datenvorverarbeitung und XGBoost-Regressor
Anmerkungen zum Code:
Frequency wird später beim Training mit der Variable Exposure gewichtetClaimAmountperClaimNb wird später beim Training mit der Variable ClaimNb gewichtet# Pipeline für Frequency
xgboost_poisson = Pipeline(
[
("preprocessor", xgboost_preprocessor_Frequency),
("regressor", XGBRegressor(max_depth=6, objective='count:poisson', importance_type='gain'))
]
)
# Pipeline für ClaimAmountperClaimNb
xgboost_gamma = Pipeline(
[
("preprocessor", xgboost_preprocessor_ClaimAmountperClaimNb),
("regressor", XGBRegressor(max_depth=2, objective='reg:gamma', importance_type='gain'))
]
)
Trainings- und Testdatensatz jeweils für Schadenswahrscheinlichkeit und Schadensaufwand erzeugen:
df_train_Freq, df_test_Freq = train_test_split(df_prep, test_size=0.3, random_state=0)
df_train_Claim, df_test_Claim = train_test_split(df_prep.dropna(subset=['ClaimAmount']), test_size=0.3, random_state=0)
# Prüfe ob Zeilenanzahl von df_train_Freq und df_test_Freq der Erwartung entspricht:
assert df_train_Freq.shape[0] + df_test_Freq.shape[0] == df_prep.shape[0], 'Zeilenanzahl unstimmig!'
assert df_train_Claim.shape[0] + df_test_Claim.shape[0] == 24944, 'Zeilenanzahl unstimmig!'
Die in den Abschnitten zuvor definierten Modelle werden nun auf Basis der Trainingsdaten trainiert/gefittet. Mit dem jeweils trainierten Modell werden anschließend die Zielgrößen-Werte vorhergesagt - sowohl bzgl. der Trainings- als auch bzgl. der Testdaten. Auf Basis der gemessenen ggü. den vorhergesagten Werten wird je Modell und pro Trainings- und Testdatensatz jeweils eine Scoring-Wert basierend auf der passenden Performance-Metrik berechnet:
U.a. anhand der Scoring-Werte kann die Modellperformance dann miteinander verglichen werden. In den danach folgenden Abschnitten finden dann zusätzlich visuelle Vergleiche der Modelle anhand der prognostizierten marginalen Werte bzgl. der Zielgrößen statt.
Zunächst erfolgt das trainieren, testen und validieren bzgl. der Modelle zur Schadenwahrscheinlichkeit, anschließend dann bzgl. der Modelle zum Schadenaufwand.
Die Modelle bzgl. Schadenwahrscheinlichkeit werden auf Basis der Trainingsdaten bzgl. der Zielgröße Frequency trainiert. Dabei dient das Merkmal Exposure als Gewichtungsfaktor.
# Fit des Dummy-Regressors bzgl. Frequency
_ = dummy_Frequency.fit(df_train_Freq, df_train_Freq["Frequency"], sample_weight=df_train_Freq["Exposure"])
# Fit des GLM bzgl. Frequency
_ = glm_poisson.fit(df_train_Freq, df_train_Freq["Frequency"], regressor__sample_weight=df_train_Freq["Exposure"])
# Fit des XGBoost-Modell bzgl. Frequency
_ = xgboost_poisson.fit(df_train_Freq, df_train_Freq["Frequency"], regressor__sample_weight=df_train_Freq["Exposure"])
Prognose und Scoring der Schadenwahrscheinlichkeit bzgl. der Trainings- und Testdaten für das jeweilige Modell
#Prognose und Scoring bzgl. Dummy-Regressor
predictAndSaveResults('dummy', dummy_Frequency, 'Frequency')
#Prognose und Scoring bzgl. GLM
predictAndSaveResults('glm_poisson', glm_poisson, 'Frequency')
#Prognose und Scoring bzgl. XGBoost
predictAndSaveResults('xgboost_poisson', xgboost_poisson, 'Frequency')
| Model_Metric_Data_Target | Value of Metric | |
|---|---|---|
| 0 | dummy_meanPoissonDeviance_df_train_Freq | 0.6267 |
| 1 | dummy_meanPoissonDeviance_df_test_Freq | 0.6192 |
| 2 | glm_poisson_meanPoissonDeviance_df_train_Freq | 0.5945 |
| 3 | glm_poisson_meanPoissonDeviance_df_test_Freq | 0.5878 |
| 4 | xgboost_poisson_meanPoissonDeviance_df_train_Freq | 0.5566 |
| 5 | xgboost_poisson_meanPoissonDeviance_df_test_Freq | 0.5720 |
Analyse der Scoringergebnisse:
Plotten der vom jeweiligen Modell prognostizierten marginalen Schadenwahrscheinlichkeiten bzgl. den Merkmalen. Zudem Darstellung ggü. den marginalen Schadenhäufigkeiten und ggü. der Schadenhäufigkeit für das gesamte Portfolio.
plotFrequenciesAndExposures(df_test_Freq, ['VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'VehBrand',
'VehGas', 'Density_temp', 'Region'], predictionValues = True)
Analyse der Plots bzgl. der prognostizierten, marginalen Schadenwahrscheinlichkeiten bzgl. den Merkmalen:
VehPower wird systematisch zu hoch von beiden Modellen prognostiziertVehAge erfolgt eine relativ gute Abbildung, inkl. des Peaks für NeuwagenDrivAge auch insgesamt gute Abbildung der Schadenhäufigkeit. XGBoost bildet bzgl. jungen Fahrern genauer ab als GLM.BonusMalus können beide Modelle den starken Trend gut abbilden.VehBrand, VehGas, Density, Region wird systematisch zu hoch von beiden Modellen prognostiziertFazit: Sowohl das GLM als auch das XGBoost-Modell können insgesamt die marginalen Schadenhäufigkeiten relativ gut abbilden. Bzgl. vieler Merkmale kommt es aber beiden Modellen zu systematisch zu hohen Prognosewerten - deshalb wird die Modellkalibrierung als nächstes untersucht werden.
Die Modelle bzgl. Schadenaufwand werden auf Basis der Trainingsdaten bzgl. der Zielgröße ClaimAmountperClaimNb trainiert. Dabei dient das Merkmal ClaimNb als Gewichtungsfaktor.
# Fit des Dummy-Regressors bzgl. ClaimAmountperClaimNb
_ = dummy_ClaimAmountperClaimNb.fit(df_train_Claim, df_train_Claim["ClaimAmountperClaimNb"], sample_weight=df_train_Claim["ClaimNb"])
# Fit des GLM bzgl. ClaimAmountperClaimNb
_ = glm_gamma.fit(df_train_Claim, df_train_Claim["ClaimAmountperClaimNb"], regressor__sample_weight=df_train_Claim["ClaimNb"])
# Fit des XGBoost-Modell bzgl. ClaimAmountperClaimNb
_ = xgboost_gamma.fit(df_train_Claim, df_train_Claim["ClaimAmountperClaimNb"], regressor__sample_weight=df_train_Claim["ClaimNb"])
Prognose und Scoring der Schadenaufwand bzgl. der Trainings- und Testdaten für das jeweilige Modell
#Prognose und Scoring bzgl. Dummy-Regressor
predictAndSaveResults('dummy', dummy_ClaimAmountperClaimNb, 'ClaimAmountperClaimNb')
#Prognose und Scoring bzgl. GLM
predictAndSaveResults('glm_gamma', glm_gamma, 'ClaimAmountperClaimNb')
#Prognose und Scoring bzgl. XGBoost
predictAndSaveResults('xgboost_gamma', xgboost_gamma, 'ClaimAmountperClaimNb')
| Model_Metric_Data_Target | Value of Metric | |
|---|---|---|
| 0 | dummy_meanGammaDeviance_df_train_Claim | 1.7672 |
| 1 | dummy_meanGammaDeviance_df_test_Claim | 1.5662 |
| 2 | glm_gamma_meanGammaDeviance_df_train_Claim | 1.5107 |
| 3 | glm_gamma_meanGammaDeviance_df_test_Claim | 1.6281 |
| 4 | xgboost_gamma_meanGammaDeviance_df_train_Claim | 1.2977 |
| 5 | xgboost_gamma_meanGammaDeviance_df_test_Claim | 1.6480 |
Analyse der Scoringergebnisse:
Das GLM und XGBoost-Modell schneiden bzgl. der Trainingsdaten deutlich besser ab als bzgl. der Testdaten. Dagegen schneidet das Dummy-Regressor-Modell bzgl. aller Modelle am Schlechtesten bzgl. der Trainings- und am Besten bzgl. der Testdaten ab. Das deutet stark darauf hin, dass die beiden anderen Modelle auf Basis der Trainingsdaten überangepasst sind und nur das Rauschen in den Trainingsdaten abgebildet haben. Die Modelle bringen somit keinen Mehrwert ggü. dem Dummy-Regressor-Modell. Generell erscheint es aber als schwierig ein Mehrwert bringendes Modell für die stark verrauschten Schadensaufwands-Daten zu finden.
Plotten der vom jeweiligen Modell prognostizierten marginalen Schadenaufwände bzgl. den Merkmalen. Zudem Darstellung ggü. den marginalen Schadenaufwänden und ggü. dem Schadenaufwand für das gesamte Portfolio.
plotClaimAmountperClaimNb(df_test_Claim, ['VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'VehBrand',
'VehGas', 'Density_temp', 'Region'], predictionValues = True)
Analyse der Plots bzgl. der prognostizierten, marginalen Schadenaufwände bzgl. der Merkmale:
Sowohl das GLM als auch das XGBoost-Modell können insgesamt die Verläufe der marginalen Schadenaufwände nicht gut abbilden. Das steht im Einklang mit der voran gegangen Erkenntnis aus der Scoringergebnisanalyse: Die Modelle bringen, in der Konfiguration und bzgl. der vorliegenden Daten, keinen Mehrwert in der Prognose ggü. einer einfachen Prognose des durchschnittlichen Schadenaufwands im Portfolio.
Nun soll untersucht werden, wie gut das jeweilige Modell kalibriert ist, d.h. ob es z.B. eine systematische Abweichung der prognostizierten Zielgröße Schadenwahrscheinlichkeit nach oben oder unten gibt im Vergleich zur gemessenen Schadenhäufigkeit. Dazu werden die prognostizierten Schadenwahrscheinlichkeiten der Reihenfolge nach angeordnet und in Gruppen mit gleicher Intervalllänge unterteilt. Bzgl. jeder Gruppe wird dann der Mittelwert der prognostizierten und der gemessenen Werte berechnet und visuell gegenüber gestellt.
Dazu wird Code mit definierter Funktion diesbzgl. aus [4] übernommen und angepasst:
def _mean_frequency_by_risk_group(y_true, y_pred, sample_weight=None, n_bins=100):
"""Compare predictions and observations for bins ordered by y_pred.
We order the samples by ``y_pred`` and split it in bins.
In each bin the observed mean is compared with the predicted mean.
Parameters
----------
y_true: array-like of shape (n_samples,)
Ground truth (correct) target values.
y_pred: array-like of shape (n_samples,)
Estimated target values.
sample_weight : array-like of shape (n_samples,)
Sample weights.
n_bins: int
Number of bins to use.
Returns
-------
bin_centers: ndarray of shape (n_bins,)
bin centers
y_true_bin: ndarray of shape (n_bins,)
average y_pred for each bin
y_pred_bin: ndarray of shape (n_bins,)
average y_pred for each bin
"""
idx_sort = np.argsort(y_pred)
bin_centers = np.arange(0, 1, 1 / n_bins) + 0.5 / n_bins
y_pred_bin = np.zeros(n_bins)
y_true_bin = np.zeros(n_bins)
for n, sl in enumerate(gen_even_slices(len(y_true), n_bins)):
weights = sample_weight[idx_sort][sl]
y_pred_bin[n] = np.average(y_pred[idx_sort][sl], weights=weights)
y_true_bin[n] = np.average(y_true[idx_sort][sl], weights=weights)
return bin_centers, y_true_bin, y_pred_bin
print(f"Anzahl gemessener Schäden in Testdaten: {df_test_Freq['ClaimNb'].sum()}")
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(12, 8))
plt.subplots_adjust(wspace=0.3)
#Definiere Modelle und Modellnamen in Liste:
models = [dummy_Frequency, glm_poisson, xgboost_poisson]
modelnames = ['dummy_Frequency', 'glm_poisson', 'xgboost_poisson']
for axi, model, modelname in zip(ax.ravel(), models, modelnames):
y_pred = model.predict(df_test_Freq)
y_true = df_test_Freq["Frequency"].values
exposure = df_test_Freq["Exposure"].values
q, y_true_seg, y_pred_seg = _mean_frequency_by_risk_group(
y_true, y_pred, sample_weight=exposure, n_bins=10
)
# Name of the model after the estimator used in the last step of the
# pipeline.
print(f"Prognostizierte Anzahl an Schäden {modelname}: {np.sum(y_pred * exposure):.1f}")
axi.plot(q, y_pred_seg, marker="x", linestyle="--", label="predictions")
axi.plot(q, y_true_seg, marker="o", linestyle="--", label="observations")
axi.set_xlim(0, 1.0)
axi.set_ylim(0, 0.5)
axi.set(
title=modelname,
xlabel="Fraction of samples sorted by y_pred",
ylabel="Mean Frequency (y_pred)",
)
axi.legend()
plt.tight_layout()
Anzahl gemessener Schäden in Testdaten: 10771.0 Prognostizierte Anzahl an Schäden dummy_Frequency: 10861.0 Prognostizierte Anzahl an Schäden glm_poisson: 10850.8 Prognostizierte Anzahl an Schäden xgboost_poisson: 10895.4
Fazit:
Auf Basis der Erkenntnisse aus der voran gegangenen Modell-Evaluierung wird folgendes Modell für die Ermittlung des Schadenbedarfs empfohlen:
Begründung:
In den nachfolgenden Abschnitten wird der Frage nach den wichtigsten Merkmalen für den Schadenbedarf nachgegangen. Dabei wird auch auf Erkenntnisse aus bereits voran gegangener Analysen zurück gegriffen, insbesondere der bereits erfolgten visuellen Analyse der marginalen Schadenwahrscheinlichkeiten.
Die Bibiliothek des XGBoost-Regressors bietet direkt eine Einwertung der Wichtigkeit der Merkmale bzgl. des voran gegangenen Trainingsprozesses an. Die Rangfolge der Merkmale beruht dabei auf dem Einfluss, den das Merkmal beim Modell-Training auf die Reduzierung des Wertes der Kostenfunktion ausgeübt hat.
featureNames = np.array(['BonusMalus', 'Density', 'VehAge', 'DrivAge', 'VehBrand', 'VehPower', 'VehGas', 'Region'])
sorted_idx = xgboost_poisson[1].feature_importances_.argsort()
plt.barh(featureNames[sorted_idx], xgboost_poisson[1].feature_importances_[sorted_idx])
plt.xlabel("XGBoost Feature Importance")
Text(0.5, 0, 'XGBoost Feature Importance')
Die Rangfolge in der Wichtigkeit der Merkmale gemäß XGBoost Feature Importance Metrik ist interessant:
BonusMalus ist das wichtigste Kriterium gemäß XGBoost Feature Importance Metrik. Da der hohe marginale Einfluss auf Schadenwahrscheinlichkeit bereits zuvor schon erkannt worden ist, ist das nicht überraschendVehGas ist 2. wichtigste Kriterium, was überraschend ist, da der Einfluss auf die marginale Schadenwahrscheinlichkeit eher gering ist. Vermutlich spielt hier ein komplizierter Zusammenhang zwischen den Merkmalen und der Zielgröße im Datensatz eine Rolle.VehAge ist 3. wichtigste Kriterium. Der Einfluss auf die marginale Schadenwahrscheinlichkeit ist gut sichtbar. Bzgl. Neuwagen liegt die Schadenhäufigkeit bei über 30%. Auch hier spielt vermutlich ein komplizierter Zusammenhang zwischen den Merkmalen und der Zielgröße im Datensatz eine Rolle.VehPower ist 4. wichtigste Kriterium. Der Einfluss auf die marginale Schadenwahrscheinlichkeit ist nicht monoton.VehBrand ist 5. wichtigste Kriterium. Da der Einfluss auf die marginale Schadenwahrscheinlichkeit bzgl. Automarke 'B12' relativ groß ist, und bzgl. dieser Automarke auch ein relativ großes Exposure vorliegt, liegt auch hier vermutlich ein komplizierter Zusammenhang zwischen den Merkmalen und der Zielgröße vor.DrivAge und Density ist überraschend, dass sie als nicht so wichtige Kriterien gewertet werden, obwohl der Einfluss auf die marginale Schadenwahrscheinlichkeit sichtbar ist.Auf Basis der Erkenntnisse bzgl. der Wichtigkeit der Merkmale für das XGBoost-Modell werden kompliziertere Zusammenhänge in den Daten bzgl. den Merkmalen und der Zielgröße Schadenwahrscheinlichkeit vermutet - beispielsweise bzgl. VehGas. Um diesen auf die Spur zu kommen, wird im folgenden ein kurzer Entscheidungsbaum gebaut und visualisiert. Die Idee dahinter ist, dass evtl. verborgene Zusammenhänge zwischen diesen Merkmalen und der Zielgröße sichtbar werden könnten. Da das gewählte XGBoost-Modell ebenfalls Entscheidungsbaum basiert ist, könnte das die durch das XGBoost-Modell ermittelte Wichtigkeit der Merkmale besser nachvollziehbar machen.
Definition des Entscheidungsbaums auf Basis der gesamten Daten df_prep:
# Definition des Daten-Preprocessors
decisonTree_preprocessor = ColumnTransformer(
[
("passthrough_numeric", "passthrough", ["BonusMalus", "Density", "VehAge", "DrivAge", "VehPower"]),
("onehot_categorical", OneHotEncoder(), ["VehBrand", "VehGas", "Region"])
],
remainder="drop"
)
# Definition der Pipeline
decisonTree = Pipeline(
[
("preprocessor", decisonTree_preprocessor),
("regressor", DecisionTreeRegressor(max_depth=3, criterion='poisson'))
]
)
# Fit des decisonTree-Modell bzgl. Frequency
_ = decisonTree.fit(df_prep, df_prep["Frequency"], regressor__sample_weight=df_prep["Exposure"])
# Zuordnung der one-hot-codierten Merkmals-Werte zu den Merkmalen
categories_array = list(decisonTree['preprocessor'].transformers_[1][1].categories_)
categories=["BonusMalus", "Density", "VehAge", "DrivAge", "VehPower"]
for category in categories_array:
for value in list(category):
categories.append(value)
assert len(categories) == (5 * 1 + df_prep.VehBrand.nunique() + df_prep.VehGas.nunique()
+ df_prep.Region.nunique()), 'Länge der Liste inkonsistent!'
# plot tree
plt.figure(figsize=(12,12))
_ = plot_tree(decisonTree[1], feature_names=categories, fontsize=8)
Analyse des Entscheidungsbaums:
VehAge, VehBrand, BonusMalus und VehGas tauchen auf den ersten 3 Ebenen auf und sind somit relativ wichtig für die Verringerung der Poisson-Kostenfunktion. Das steht relativ gut im Einklang mit der ermittelten Rangfolge der Merkmale bzgl. der Wichtigkeit für das XGBoost-Modell.Aus den voran gegangen Analysen bzgl. der wichtigsten Merkmale lässt sich schlussfolgern:
VehAge, Fahrzeugmarke VehBrand, Fahrer-Score BonusMalus und Kraftstoff VehGas spielen eine wichtige Rolle bzgl. der Vorhersage der Schadenwahrscheinlichkeit und damit auch für den finalen Schadenbedarf (der Schadenaufwand pro Schadensfall wird ja als unabhängig von den Risikomerkmalen angenommen).VehBrand und Kraftstoff VehGas deutlich überschätzt wird, da hier vermutlich die spezielle Hochrisiko-Gruppe "Neuwagen der Automarke B12 mit normalem Kraftstoff" einen großen Einfluss hat - dies könnte noch weiter untersucht werden, z.B. durch Berücksichtigung eines zusätzlichen synthetischen Merkmals aus diesen 3 Merkmalen, welches dann diese Gruppe kennzeichnet.BonusMalus und Fahrzeugalter VehAge auf den finalen Schadenbedarf gibt es dagegen mehrere Indizien: Beide Merkmale haben ein trendförmiges marginales Schadenwahrscheinlichkeitsprofil und beide werden auch von den baumbasierten Verfahren als wichtige Merkmale zur Reduzierung der Kostenfunktion berücksichtigt.